﻿package com.zouloux.persp 
{
	import com.zouloux.display.DocumentClass;
	import com.zouloux.events.RaEvent;
	import flash.display.Bitmap;
	import flash.display.BitmapData;
	import flash.display.Sprite;
	import flash.events.TimerEvent;
	import flash.filters.BlurFilter;
	import flash.geom.Point;
	import flash.geom.Rectangle;
	import flash.media.Camera;
	import flash.media.Video;
	import flash.utils.ByteArray;
	import flash.utils.clearTimeout;
	import flash.utils.setTimeout;
	import flash.utils.Timer;
	import org.libspark.flartoolkit.core.FLARCode;
	import org.libspark.flartoolkit.core.param.FLARParam;
	import org.libspark.flartoolkit.core.raster.rgb.FLARRgbRaster_BitmapData;
	import org.libspark.flartoolkit.core.transmat.FLARTransMatResult;
	import org.libspark.flartoolkit.detector.FLARMultiMarkerDetector;
	import org.libspark.flartoolkit.detector.FLARSingleMarkerDetector;
	import org.libspark.flartoolkit.pv3d.FLARBaseNode;
	import org.libspark.flartoolkit.pv3d.FLARCamera3D;
	
	/**
	 * ...
	 * @author ZoulouX
	 */
	public class RaEngine extends Sprite
	{
		// Les dimensions de sampling
		protected var _sampleHeight				:uint;
		protected var _sampleWidth				:uint;
		
		// Les dimensions de la camera
		protected var _cameraWidth				:uint;
		protected var _cameraHeight				:uint;
		
		// Les dimensions de la vidéo
		protected var _videoWidth				:uint;
		protected var _videoHeight				:uint;
		
		// La fréquence de rafraichissement pour la camera
		protected var _cameraRate				:uint;
		
		// La vidéo et son container
		protected var _video					:Video;
		protected var _containerVideo			:Sprite;
		protected var _containerVideoRaster		:Sprite;
		
		// L'objet camera et ses paramètres
		protected var _camera					:Camera;
		protected var _reversed					:Boolean						= false;
		
		// Le bitmap
		protected var _bitmapData				:BitmapData;
		
		// Le raster bitmap
		protected var _raster					:FLARRgbRaster_BitmapData;
		
		// Les paramètres de la cam
		protected var _flarParams				:FLARParam;
		
		// Les détécteurs
		protected var _detectorMulti			:FLARMultiMarkerDetector;
		protected var _detectorSingle			:FLARSingleMarkerDetector;
		
		// Le marker séléctionné
		protected var _selectedMarker			:uint							= 0;
		
		// La liste des markers
		protected var _markers					:Array							= [];
		protected var _markersSize				:Array							= [];
		protected var _markersConfidence		:Array							= [];
		
		// Le tableau des timeOuts
		protected var _markerTimers				:Array							= [];
		
		// La liste des scènes 3D
		protected var _scenes					:Array							= [];
		
		// Le timer pour le sampling
		protected var _timer					:Timer;
		
		// Le moteur 3D
		protected var _paperEngine				:PaperEngine;
		
		// La camera pour Flar
		protected var _cameraFlar				:FLARCamera3D;
		
		// L'objet de gestion de la transformation FLAR
		protected var _transformFlar			:FLARTransMatResult;
		
		// La limite de détéction
		protected var _threshold				:uint							= 82;
		
		// Mode multi
		protected var _multiMode				:Boolean						= false;
		
		// Les patterns en vue
		protected var _visiblePatterns			:Array							= [];
		
		// Définir si on doit masquer les patterns automatiquement
		protected var _autoVisible				:Boolean						= true;
		
		// Définir si on doit rasteriser la webcam
		protected var _rasterWebcam				:Boolean						= false;
		
		// Le bitmap de la webcam
		protected var _webcamBitmap				:Bitmap;
		protected var _bitmapDataVideo			:BitmapData;
		
		// Le délais de timeout pour les markers
		protected var _timeOut					:uint							= 0;
		
		// Le flou avant le thresholding
		protected var _blur						:uint							= 0;
		
		// La couche 2D
		protected var _sprite2D					:Sprite							= new Sprite();
		
		
		// Les getters
		public function get sampleRate ():uint
		{
			return _timer.delay * 1000;
		}
		public function get cameraRate():uint
		{
			if (_camera != null)
				return _camera.fps;
			else
				return _cameraRate;
		}
		public function get paperEngine():PaperEngine
		{
			return _paperEngine;
		}
		public function get scenes ():Array
		{
			return _scenes;
		}
		public function get selectedMarker():uint
		{
			return _selectedMarker;
		}
		public function get threshold():uint
		{
			return _threshold;
		}
		public function get multi():Boolean
		{
			return _multiMode;
		}
		public function get autoVisible():Boolean
		{
			return _autoVisible;
		}
		public function get rasterCamera():Boolean
		{
			return _rasterWebcam;
		}
		public function get timeOut():uint
		{
			return _timeOut;
		}
		public function get blur():uint
		{
			return _blur;
		}
		public function get camera3d():FLARCamera3D
		{
			return _cameraFlar;
		}
		public function get reversed():Boolean
		{
			return _reversed;
		}
		public function get bitmapDataVideo():BitmapData
		{
			return _bitmapDataVideo;
		}
		
		
		// Les setters
		public function set sampleRate (value:uint):void
		{
			_timer.delay = 1000 / value;
		}
		public function set cameraRate (value:uint):void
		{
			_cameraRate = value;
			if (_camera != null)
				_camera.setMode(_camera.width, _camera.height, value, true);
		}
		public function set scenes (value:Array):void
		{
			_scenes = value;
		}
		public function set selectedMarker(value:uint):void 
		{
			if (value >= _markers.length)
				value = _markers.length - 1;
			_selectedMarker = value;
		}
		public function set threshold(value:uint):void 
		{
			_threshold = value;
		}
		public function set multi(value:Boolean):void 
		{
			_multiMode = value;
		}
		public function set autoVisible(value:Boolean):void 
		{
			_autoVisible = value;
		}
		public function set rasterCamera(value:Boolean):void 
		{
			if (_rasterWebcam == false && value == true)
			{
				// Créer le bitmap
				_bitmapDataVideo = new BitmapData(_videoWidth, _videoHeight, false, 0);
				_webcamBitmap = new Bitmap(_bitmapDataVideo);
				_webcamBitmap.width = _videoWidth;
				_webcamBitmap.height = _videoHeight;
				_webcamBitmap.smoothing = false;
				
				// Et l'ajouter
				removeChild(_containerVideoRaster);
				addChildAt(_webcamBitmap, 0);
			}
			else if (_rasterWebcam == true && value == false)
			{
				// Enlever le bitmap
				removeChild(_webcamBitmap);
				
				// Remettre le raster
				addChildAt(_containerVideoRaster, 0);
			}
			_rasterWebcam = value;
		}
		public function set timeOut(value:uint):void 
		{
			_timeOut = value;
		}
		public function set blur(value:uint):void 
		{
			_blur = value;
		}
		public function set reversed(value:Boolean):void 
		{
			_reversed = value;
			
			// Efféctuer l'inversion
			if (_reversed)
			{
				// Si on est en affichage webcam bitmap
				if (_rasterWebcam)
				{
					_webcamBitmap.scaleX = -1;
					_webcamBitmap.x = _videoWidth;
				}
				else
				{
					_containerVideoRaster.scaleX = -1;
					_containerVideoRaster.x = _videoWidth;
				}
				
				// Le moteur 3D
				_paperEngine.scaleX = -1;
				_paperEngine.x = _videoWidth;
			}
			else
			{
				// Si on est en affichage webcam bitmap
				if (_rasterWebcam)
				{
					_webcamBitmap.scaleX = 1;
					_webcamBitmap.x = 0;
				}
				else
				{
					_containerVideoRaster.scaleX = 1;
					_containerVideoRaster.x = 0;
				}
				
				// Le moteur 3D
				_paperEngine.scaleX = 1;
				_paperEngine.x = 0;
			}
		}
		public function get sprite2D():Sprite
		{
			return _sprite2D;
		}
		
		
		
		// Le constructeur
		public function RaEngine() 
		{
			// Créer le timer
			_timer = new Timer(1000 / DocumentClass.stage.frameRate);
			_timer.addEventListener(TimerEvent.TIMER, sample);
			
			// Définir la fréquence de la webcam
			_cameraRate = DocumentClass.stage.frameRate;
			
			// Les tailles par défaut
			setVideoSize();
			setCameraSize();
			setSamplingSize();
		}
		
		// Définition des dimensions
		public function setVideoSize (pWidth:int = 640, pHeight:int = 480):void
		{
			_videoWidth = pWidth;
			_videoHeight = pHeight;
		}
		public function setCameraSize (pWidth:int = 320, pHeight:int = 240):void
		{
			_cameraWidth = pWidth;
			_cameraHeight = pHeight;	
		}
		public function setSamplingSize (pWidth:int = 320, pHeight:int = 240):void
		{
			_sampleWidth = pWidth;
			_sampleHeight = pHeight;
		}
		
		// Changer le taux de rafraichissement
		public function setRate (pValue:uint):void
		{
			sampleRate = pValue;
			cameraRate = pValue;
		}
		
		// Initialiser la camera
		public function initCamera (pCameraName:String = ""):void
		{
			// Créer le conteneur vidéo
			_video = new Video(_sampleWidth, _sampleHeight);
			_video.smoothing = false;
			_video.deblocking = 1;
			
			// Créer la cam et la configurer
			_camera = Camera.getCamera(pCameraName == "" ? null : pCameraName);
			_camera.setMode(_cameraWidth, _cameraHeight, _cameraRate, true);
			
			// Attacher la camera à l'objet vidéo pour l'afficher
			_video.attachCamera(_camera);
			
			// Ajouter la vidéo à travers un container
			_containerVideo = new Sprite();
			_containerVideo.addChild(_video);
			
			// Créer le raster (pour la synchro)
			_containerVideoRaster = new Sprite();
			_containerVideoRaster.addChild(_containerVideo);
			
			// Ajouter le container vidéo raster
			addChild(_containerVideoRaster);
			
			// Redimentionner la video
			_containerVideo.scaleX = _videoWidth / _sampleWidth;
			_containerVideo.scaleY = _videoHeight / _sampleHeight;
			
			// Centrer le sprite
			_sprite2D.x = _containerVideo.width / 2; 
			_sprite2D.y = _containerVideo.height / 2;
			
			// Créer le moteur 3D
			_paperEngine = new PaperEngine(_videoWidth, _videoHeight);
			
			// Ajouter le moteur
			addChild(_paperEngine);
			addChild(_sprite2D);
		}
		
		// Initialiser le sampling
		public function initSample (pParams:Class):void
		{
			// Créer le bitmapData et dessiner la vidéo
			_bitmapData = new BitmapData(_sampleWidth, _sampleHeight, false, 0);
			updateBitmap();
			
			// Paramétrer Flar
			_flarParams = new FLARParam();
			_flarParams.loadARParam(new pParams());
			_flarParams.changeScreenSize(_sampleWidth, _sampleHeight);
			
			// Créer le raster
			_raster = new FLARRgbRaster_BitmapData(_bitmapData);
			
			// Créer le détécteur simple ou multi
			_detectorMulti = new FLARMultiMarkerDetector(_flarParams, _markers, _markersSize, _markers.length);
			_detectorSingle = new FLARSingleMarkerDetector(_flarParams, _markers[_selectedMarker], _markersSize[_selectedMarker]);
			
			// Ajouter la caméra FLAR
			_cameraFlar = new FLARCamera3D(_flarParams);
			_paperEngine.addCamera(_cameraFlar);
			
			// Recaler la 3D (correction bug)
			_paperEngine.viewport.x = -_videoWidth / 160;
			
			// L'objet de transformation 3D
			_transformFlar = new FLARTransMatResult();
		}
		
		// Ajouter un marker
		public function addMarker (pMarker:Class, pWidth:int = 16, pRatio:int = 50, pConfidence:Number = 0.5):uint
		{
			// Créer
			var flarCode:FLARCode = new FLARCode(pWidth, pWidth);
			flarCode.loadARPatt(new pMarker());
			
			// Ajouter
			_markers.push(flarCode);
			_markersSize.push(pRatio);
			_markersConfidence.push(pConfidence);
			
			// Ajouter la scène qui va avec
			var lastScene:FLARBaseNode = new FLARBaseNode();
			_scenes.push(lastScene);
			_paperEngine.scene.addChild(lastScene);
			
			// Si on est en autoVisible
			if (_autoVisible)
				lastScene.visible = false;
			
			// Retourner
			return _scenes.length - 1;
		}
		
		// Supprimer un marker
		public function deleteMarker (pMarker:Class):void
		{
			var totalMarkers	:int 	= _markers.length;
			var newMarkers		:Array 	= [];
			var newSizes		:Array 	= [];
			var newScenes		:Array 	= [];
			var newConfidences	:Array 	= [];
			for (var i:int = 0; i < totalMarkers; i++)
			{
				if (_markers[i].toString() != (new pMarker() as ByteArray).toString())
				{
					newMarkers.push(_markers[i]);
					newSizes.push(_markersSize[i]);
					newScenes.push(_scenes[i]);
					newConfidences.push(_markersConfidence[i]);
				}
			}
			if (newMarkers.length != _markers.length)
			{
				_markers 			= newMarkers;
				_markersSize 		= newSizes;
				_scenes 			= newScenes;
				_markersConfidence = newConfidences;
			}
		}
		
		// Démarrer le sampling
		public function startSample ():void
		{
			_timer.start();
		}
		
		// Arrêter le sampling
		public function stopSample ():void
		{
			_timer.stop();
		}
		
		// Faire un sampling
		private function sample (event:TimerEvent = null):void
		{
			// On actualise l'image
			updateBitmap();
			
			// Lancer la détéction
			if (_multiMode)
			{
				// Détécter
				var detectMulti:int = 0;
				try
				{
					detectMulti = _detectorMulti.detectMarkerLite(_raster, _threshold);
				}
				catch (e:Error)
				{
					trace("ERROR: "+e);
				}
				
				// Détécter en mode multi
				if (detectMulti > 0)
				{
					// Les markers qui ont été détéctés à cette passe
					var markersOk:Array = [];
					
					// Parcourir tous les markers
					for (var i:int = 0; i < detectMulti; i++)
					{
						// Récupérer l'exactitude
						var conf:Number = _detectorMulti.getConfidence(i);
						
						// Récupérer l'index de la scène
						var index:int = _detectorMulti.getARCodeIndex(i);
						
						// Vérifier l'exactitude
						if (conf > _markersConfidence[index])
						{
							// Enregistrer l'exactitude la plus forte
							if (markersOk[index] == null)
								markersOk[index] = [i, conf];
							else if (conf > markersOk[index][1])
								markersOk[index] = [i, conf];
						}
					}
					
					// Reparcourir
					for (var i3:int = 0; i3 < _markers.length; i3++)
					{
						if (markersOk[i3] == null)
						{
							// Virer les markers non détéctés
							patternOut(i3);
						}
						else
						{
							// Récupérer l'objet de transformation
							_detectorMulti.getTransmationMatrix(markersOk[i3][0], _transformFlar);
							
							// L'appliquer au container
							_scenes[i3].setTransformMatrix(_transformFlar);
							
							// Afficher
							patternIn(i3);
						}
					}
				}
				else
				{
					// Tout masquer
					for (var i2:int = 0; i2 < _markers.length; i2++)
					{
						patternOut(i2);
					}
				}
			}
			else
			{
				// Détécter
				var detectSingle:Boolean = false;
				try
				{
					detectSingle = _detectorSingle.detectMarkerLite(_raster, _threshold);
				}
				catch (e:Error)
				{
					trace("ERROR: "+e);
				}
				
				// Détécter en mode simple
				if (detectSingle)
				{
					// Vérifier l'exactitude
					if (_detectorSingle.getConfidence() > _markersConfidence[_selectedMarker])
					{
						// Récupérer l'objet de transformation
						_detectorSingle.getTransformMatrix(_transformFlar);
						
						// L'appliquer au container
						_scenes[_selectedMarker].setTransformMatrix(_transformFlar);
						
						// Dispatcher
						patternIn(_selectedMarker);
					}
					else
						patternOut(_selectedMarker);
				}
				else
					patternOut(_selectedMarker);
			}
			
			// Vérifier si on doit actualiser le raster de la webcam
			if (_rasterWebcam)
				_bitmapDataVideo.draw(_containerVideoRaster);
			
			// Et on dispatche l'event
			dispatchEvent(new RaEvent(RaEvent.SAMPLE));
			
			// Actualiser la 3D
			_paperEngine.update();
		}
		
		// Pattern visible
		protected function patternIn (pIndex:int):void
		{
			// Vérifier si on doit tuer le timeOut
			if (_markerTimers[pIndex] != null && _timeOut > 0)
			{
				trace("CLEAR " + pIndex);
				
				// Effacer ce timeout
				clearTimeout(_markerTimers[pIndex]);
				_markerTimers[pIndex] = null;
			}
			else
			{
				// Vérifier si ce pattern n'est pas déjà visible
				if (_visiblePatterns[pIndex] == null || _visiblePatterns[pIndex] == false)
				{
					// Dispatcher l'évènement
					var raEvent:RaEvent = new RaEvent(RaEvent.PATTERN_IN);
					raEvent.pattern = pIndex;
					dispatchEvent(raEvent);
					
					// Maintenant il est visible
					_visiblePatterns[pIndex] = true;
					
					// Afficher
					if (_autoVisible)
						_scenes[pIndex].visible = true;
				}
			}
		}
		
		// Pattern invisible
		protected function patternOut (pIndex:int):void
		{
			// Vérifier si ce pattern n'est pas déjà visible
			if (_visiblePatterns[pIndex] != null && _visiblePatterns[pIndex] == true)
			{
				// Vérifier si on doit appliquer un timeout
				if (_timeOut > 0)
				{
					// Vérifier s'il n'y en a pas déjà un
					if (_markerTimers[pIndex] == null)
					{
						trace("START TIME OUT FOR "+pIndex+" ("+_timeOut+")")
						_markerTimers[pIndex] = setTimeout(dispatchPatternOutEvent, _timeOut, pIndex);	
					}
				}
				else
					dispatchPatternOutEvent(pIndex);
			}
		}
		
		// Dispatcher un event de pattern out
		protected function dispatchPatternOutEvent(pIndex:int):void
		{
			trace("TIME OUT " + pIndex);
			
			// Virer le timeout dans tous les cas
			clearTimeout(_markerTimers[pIndex]);
			_markerTimers[pIndex] = null;
			
			// Dispatcher l'évènement
			var raEvent:RaEvent = new RaEvent(RaEvent.PATTERN_OUT);
			raEvent.pattern = pIndex;
			dispatchEvent(raEvent);
			
			// Maintenant il est invisible
			_visiblePatterns[pIndex] = false;
			
			// Masquer
			if (_autoVisible)
				_scenes[pIndex].visible = false;
		}
		
		// Actualiser depuis l'extérieur
		public function update ():void
		{
			sample();
		}
		
		// Actutaliser le bitmap
		public function updateBitmap ():void
		{
			// Dessiner le bitmap
			_bitmapData.draw(_containerVideo);
			
			// Vérifier s'il y du flou à ajouter
			if (_blur != 0)
				_bitmapData.applyFilter(_bitmapData, _bitmapData.rect, new Point(), new BlurFilter(_blur, _blur, 2));
		}
		
		// Récupérer la couleur d'un pixel
		public function getPixel (x:int, y:int):uint
		{
			var newX:uint = (x + _videoWidth / 2) * (_sampleWidth / _videoWidth);
			var newY:uint = (y + _videoHeight / 2) * (_sampleHeight / _videoHeight);
			
			return _bitmapData.getPixel(newX, newY);
		}
	}
}